package svv.projet.purse;

import javacard.framework.APDU;
import javacard.framework.Applet;
import javacard.framework.ISOException;
import javacard.framework.ISO7816;
import javacard.framework.OwnerPIN;
import javacard.framework.Util;

public class purseApplet extends Applet {
	// On definit des constantes pour faciliter l'ecriture des programmes

	OwnerPIN userPin;
	OwnerPIN bankPin;
	// Constante pour le CLA
	public static final byte MON_CLA = (byte) 0x25;

	//Etat de la carte
	public static final byte PRE_PERSO = 1;
	public static final byte USE = 2;
	public static final byte INVALID = -2;
	public static final byte DEAD = -1;


	// Une constante par INS, c'est a dire par commande
	public static  final byte INS_CREDIT = (byte) 0x42;
	public static  final byte INS_DEBIT = (byte) 0x43;
	public static  final byte INS_VERIFY_PIN = (byte) 0x44;
	public static  final byte INS_GET_DATA = (byte) 0x45;
	public static  final byte INS_PUT_DATA = (byte) 0x47;
	public static  final byte INS_VERIFY_BANK_PIN = (byte) 0x48;
	public static  final byte INS_GET_DATA_NOT_VERIFIED = (byte) 0x49;

	// Constantes de status word error
	private byte SW_STATUS_ERROR = (byte) 0x1e;
	private byte SW_VERIFY_PIN_ERROR = (byte) 0x2e;
	private byte SW_CREDIT_ERROR = (byte) 0x5e;
	private byte SW_TAILLE_ERROR = (byte) 0x3e;
	private byte SW_PUT_DATA_USER_PIN_ERROR = (byte) 0x4e;
	private byte SW_CREDIT_SIZE_ERROR = (byte) 0x5e;
	private byte SW_DEBIT_SIZE_ERROR = (byte) 0x5d;
	private byte SW_PIN_NOT_VERIFIED = (byte) 0x6e;
	private byte SW_CREDIT_AMOUNT_ERROR = (byte) 0x7e;
	private byte SW_DEBIT_AMOUNT_ERROR = (byte) 0x7d;
	private byte SW_CREDIT_TOTAL_AMOUNT_ERROR = (byte) 0x8e;
	private byte SW_DEBIT_NEGATIVE_AMOUNT_ERROR = (byte) 0x8d;
	private byte SW_GET_DATAS_TYPE_ERROR = (byte) 0x9e;
	private byte SW_INS_ERROR = (byte) 0x10;


	// Autres constantes (constantes fonctionnelles)
	// nombres essais possibles
	private final byte MAX_USER_TRIES;
	private final byte MAX_BANK_TRIES;
	// balance max sur le compte
	private final short MAX_BALANCE;

	// maximum par transaction
	private final short MAX_CREDIT_AMOUNT;
	private final short MAX_DEBIT_AMOUNT;

	// description des attributs
	private byte balance;
	private byte nbUserTriesLeft;
	private byte nbBankTriesLeft;
	private byte nbTransLeft;
	private boolean userIdentified;
	private boolean bankIdentified;
	private byte STATUS;

	// constructeur (optionnel)
	public purseApplet() {
		balance = 0;
		nbUserTriesLeft = 3;
		nbBankTriesLeft = 10;
		nbTransLeft = 50;
		userIdentified = false;
		bankIdentified = false;
		MAX_USER_TRIES = 3;
		MAX_BANK_TRIES = 10;
		MAX_BALANCE = 100;
		MAX_CREDIT_AMOUNT = 40;
		MAX_DEBIT_AMOUNT = 40;
		STATUS = PRE_PERSO;
		byte [] bankPinTab = {1,2,3,4,5,6};
		this.bankPin = new OwnerPIN(MAX_BANK_TRIES,(byte) 6);
		this.bankPin.update(bankPinTab, (short)0, (byte)bankPinTab.length);
	}

	public static void install(byte[] tab, short debut, byte long1) throws ISOException {
		purseApplet myapp = new purseApplet();
		myapp.register();
	}

	public boolean select() {
		return true;
	}

	/**
	 * Méthode principale qui gère les APDU reçues. On teste la
	 * validité du format de l'APDU et on appelle la méthode 
	 * correspondante à la partie INS.
	 *
	 * @param  apdu		l'APDU à traiter
	 */	
	public void process(APDU apdu) throws ISOException {
		if(STATUS == DEAD){
			ISOException.throwIt(SW_STATUS_ERROR);
		}
		// recuperation de l'APDU sous forme d'un tableau :
		byte[] buffer = apdu.getBuffer();
		if (this.selectingApplet()) return;
		// on verifie que le CLA correspond a celui attendu (ici 0x25) :
		if (buffer[ISO7816.OFFSET_CLA] != MON_CLA) {
			ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
		}
		// un switch sur le INS permet de decrire le code d'une commande (d'un INS) :
		switch(buffer[ISO7816.OFFSET_INS]) {
		// commande de personnalisation du user PIN
		case INS_PUT_DATA:
			put_data_user_pin(apdu);
			break;

			// commande de credit
		case INS_CREDIT:
			// on verifie que l'utilisateur est identifie
			if(!userPin.isValidated()){
				ISOException.throwIt(SW_PIN_NOT_VERIFIED);
			}
			credit(apdu); // appel de la methode correspondante
			userPin.reset();
			break;

			// commande de debit
		case INS_DEBIT:
			debit(apdu);
			break;

			// commande de verification de pin
		case INS_VERIFY_PIN:
			if(userPin.isValidated()){
				ISOException.throwIt(SW_VERIFY_PIN_ERROR);
			}
			verify_pin(apdu);
			break;

			// commande de verification de pin
		case INS_VERIFY_BANK_PIN:
			verify_bank_pin(apdu);
			break;

			// commande de recuperation des donnees publiques
		case INS_GET_DATA:
			// on verifie que l'utilisateur est identifie
			if(!userPin.isValidated()){
				ISOException.throwIt(SW_PIN_NOT_VERIFIED);
			}
			getDatas(apdu);
			break;
		case INS_GET_DATA_NOT_VERIFIED:
			getDatasNotVerif(apdu);
			break;

		default: // si le INS ne correspond pas a une commande prevue
			ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);

		} // fin de la description des commandes

	}

	/**
	 * Effectue une operation de debit en recuperant dans l'APDU 
	 * le montant a debiter.
	 * Renvoie des SW d'erreurs explicite ou 9000 en cas de succes.
	 *
	 * @param  apdu		l'APDU à traiter
	 */	
	private void debit(APDU apdu) {
		nbTransLeft--;
		// on verifie que le statut permet un credit
		if(STATUS != USE){
			ISOException.throwIt(SW_STATUS_ERROR);
		}

		// on commence par recuperer le contenu de l'APDU :
		byte[] buffer = apdu.getBuffer();

		// on lit ensuite la taille attendue des donnees optionnelles 
		// contenues dans l'APDU
		byte lengthLC = buffer[ISO7816.OFFSET_LC];

		// on lit la taille reelle des donnees optionnelles
		byte sizeRead = (byte) apdu.setIncomingAndReceive();

		// la taille lue et attendue doit être d'1 byte,  
		// sinon on renvoie un status word d'erreur
		if (sizeRead != 1 || lengthLC != 1){
			ISOException.throwIt(SW_DEBIT_SIZE_ERROR);
		}
		// sinon, on peut lire le montant a debiter
		byte debitAmount = buffer[ISO7816.OFFSET_CDATA];

		// on renvoie un status word erreur si le montant est trop eleve
		// ou si la balence+credit depasse MAX_BAlANCE
		if (debitAmount > MAX_DEBIT_AMOUNT){
			ISOException.throwIt(SW_DEBIT_AMOUNT_ERROR );
		}
		else{
			if(balance - debitAmount < 0){
				ISOException.throwIt(SW_DEBIT_NEGATIVE_AMOUNT_ERROR );
			}
			else{
				// sinon on debite
				balance -= debitAmount;
			}
		}
	}

	/**
	 * Effectue une opÃ©ration de credit en recuperant dans l'APDU 
	 * le montant a crediter.
	 * Renvoie des SW d'erreurs explicite ou 9000 en cas de succes.
	 *
	 * @param  apdu		l'APDU Ã  traiter
	 */	
	public void credit(APDU apdu) {
		nbTransLeft--;
		// on verifie que le statut permet un credit
		if(STATUS != USE){
			ISOException.throwIt(SW_STATUS_ERROR);
		}

		// on commence par recuperer le contenu de l'APDU :
		byte[] buffer = apdu.getBuffer();

		// on lit ensuite la taille attendue des donnees optionnelles 
		// contenues dans l'APDU
		byte lengthLC = buffer[ISO7816.OFFSET_LC];

		// on lit la taille reelle des donnees optionnelles
		byte sizeRead = (byte) apdu.setIncomingAndReceive();

		// la taille lue et attendue doit Ãªtre d'1 byte,  
		// sinon on renvoie un status word d'erreur
		if (sizeRead != 1 || lengthLC != 1){
			ISOException.throwIt(SW_CREDIT_SIZE_ERROR);
		}
		// sinon, on peut lire le montant a crediter
		byte creditAmount = buffer[ISO7816.OFFSET_CDATA];

		// on renvoie un status word erreur si le montant est trop eleve
		// ou si la balence+credit depasse MAX_BAlANCE
		if (creditAmount > MAX_CREDIT_AMOUNT){
			ISOException.throwIt(SW_CREDIT_AMOUNT_ERROR );
		}
		else{
			if(balance + creditAmount > MAX_BALANCE){
				ISOException.throwIt(SW_CREDIT_TOTAL_AMOUNT_ERROR );
			}
			else{
				// sinon on credite
				balance += creditAmount;
			}
		}
	}

	/**
	 * Verification du pin de l'utilisateur uniquement si le pin utilisateur n'a pas été deja vérifié
	 * 
	 * @param apdu APDu a utiliser
	 */
	public void verify_pin(APDU apdu){
		if(STATUS != USE)
			ISOException.throwIt(SW_STATUS_ERROR);

		// on commence pour recuperer le contenu de l'APDU :
		byte[] buffer = apdu.getBuffer();
		byte[] pinToVerify = getBytes(apdu, buffer);

		if(!this.userPin.check(pinToVerify,(short)0,(byte)pinToVerify.length)){
			if(userPin.getTriesRemaining() == 0)
				STATUS = INVALID;
			
			ISOException.throwIt(SW_VERIFY_PIN_ERROR);
		}

	}

	/**
	 * Verification du pin de la banque uniquement si le pin utilisateur est bloque
	 * dans le but de le debloquer et de remetre le cycle a USE
	 * 
	 * @param apdu APDu a utiliser
	 */
	public void verify_bank_pin(APDU apdu){
		if(STATUS != INVALID)
			ISOException.throwIt(SW_STATUS_ERROR);

		// on commence pour recuperer le contenu de l'APDU :
		byte[] buffer = apdu.getBuffer();
		byte[] pinToVerify = getBytes(apdu, buffer);

		if(!this.bankPin.check(pinToVerify,(short)0,(byte)pinToVerify.length)){
			if(bankPin.getTriesRemaining() == 0)
				STATUS = DEAD;

			ISOException.throwIt(SW_VERIFY_PIN_ERROR);
		}
		bankPin.reset();
		userPin.resetAndUnblock();
		STATUS = USE;
	}
	/**
	 * Initialise le pin utilisateur a la valeur passée en parametre
	 * et change le cycle de viee de la carte a USE
	 * Renvoie des SW d'erreurs explicite ou 90 en cas de succes.
	 * 
	 * @param  apdu		l'APDU Ã  traiter
	 */
	public void put_data_user_pin(APDU apdu){
		if(STATUS != PRE_PERSO)
			ISOException.throwIt(SW_STATUS_ERROR);
		// on commence pour recuperer le contenu de l'APDU :
		byte[] buffer = apdu.getBuffer();
		// sinon, on peut lire le pin a mettre
		byte[] pinToinsert = getBytes(apdu, buffer);
		this.userPin = new OwnerPIN(MAX_USER_TRIES,(byte) 4);
		this.userPin.update(pinToinsert, (short)0, (byte)pinToinsert.length);
		STATUS = USE;
	}

	/**
	 * Renvoie un tableau de byte contenant uniquement les donnees contenues
	 * dans l'APDU 
	 *
	 * @param  apdu		l'APDU Ã  traiter
	 * @param  buffer	tableau de bytes correspondant Ã  apdu.getBuffer()
	 * @return			sous-tableau de apdu contenant uniquement les donnees
	 */
	private byte[] getBytes(APDU apdu, byte[] buffer){
		byte[] buf;
		byte lengthLC = buffer[ISO7816.OFFSET_LC];
		byte sizeRead = (byte) apdu.setIncomingAndReceive();

		if (sizeRead != lengthLC || lengthLC == 0)
			ISOException.throwIt(SW_TAILLE_ERROR);

		buf = new byte[sizeRead];
		Util.arrayCopy(buffer, ISO7816.OFFSET_CDATA, buf, (byte)0, sizeRead);
		return buf;
	}


	/**
	 * Recuperation d'une donn�e selon sont type dans le param P1 de l'apdu
	 * 
	 * @param apdu APDu a utiliser
	 */
	//Choix de la donne a recuperer 1:solde, 2: nombre de transaction restantes, 3:nombre d'essai du code utilisateur
	public void getDatas(APDU apdu){
		if(STATUS != USE)
			ISOException.throwIt(SW_STATUS_ERROR);

		short Le = apdu.setOutgoing();
		if ( Le < 1 )
			ISOException.throwIt(SW_TAILLE_ERROR);

		apdu.setOutgoingLength((byte)1);

		byte buffer[] = apdu.getBuffer();

		byte P1 = buffer[ISO7816.OFFSET_P1];
		if (P1!= (byte)1 && P1!= (byte)2)
			ISOException.throwIt(SW_GET_DATAS_TYPE_ERROR);

		if(P1 == (byte)1){
			buffer[ISO7816.OFFSET_CDATA] = balance;
		}
		if(P1 == (byte)2){
			buffer[ISO7816.OFFSET_CDATA] = nbTransLeft;
		}
		
	
		apdu.sendBytes(ISO7816.OFFSET_CDATA, (short)1);

	}
	
	public void getDatasNotVerif(APDU apdu){
		if(STATUS != USE)
			ISOException.throwIt(SW_STATUS_ERROR);

		short Le = apdu.setOutgoing();
		if ( Le < 1 )
			ISOException.throwIt(SW_TAILLE_ERROR);

		apdu.setOutgoingLength((byte)1);

		byte buffer[] = apdu.getBuffer();

		byte P1 = buffer[ISO7816.OFFSET_P1];
		if (P1!= (byte)3)
			ISOException.throwIt(SW_GET_DATAS_TYPE_ERROR);

		if(P1 == (byte)3){
			buffer[ISO7816.OFFSET_CDATA] = userPin.getTriesRemaining();
		}
		apdu.sendBytes(ISO7816.OFFSET_CDATA, (short)1);

	}
	


}